Random integer (discrete uniform) distribution (randint)#
The randint distribution models an integer drawn uniformly at random from a finite range of consecutive integers.
In SciPy, it corresponds to scipy.stats.randint(low, high) with support:
$\(
\{\texttt{low},\ \texttt{low}+1,\ \ldots,\ \texttt{high}-1\}.
\)$
Learning goals#
Recognize when a bounded integer uniform model is appropriate.
Write the PMF and CDF carefully (including the half-open interval convention).
Compute mean/variance/skewness/kurtosis and entropy.
Derive the likelihood and the MLE for the bounds.
Implement sampling from scratch (NumPy-only) and validate it by simulation.
Use
scipy.stats.randintfor PMF/CDF/sampling and understand how to fit withscipy.stats.fit.
Prerequisites#
Basic probability (PMF/CDF), expectation, and variance
Familiarity with sums like \(\sum_{k=0}^{n-1} k\) and \(\sum_{k=0}^{n-1} k^2\)
Comfort with logs and basic optimization intuition
Notebook roadmap#
Title & Classification
Intuition & Motivation
Formal Definition
Moments & Properties
Parameter Interpretation
Derivations (Expectation, Variance, Likelihood)
Sampling & Simulation (NumPy-only)
Visualization (PMF, CDF, Monte Carlo)
SciPy Integration
Statistical Use Cases
Pitfalls
Summary
import numpy as np
import plotly.graph_objects as go
import os
import plotly.io as pio
import scipy
from scipy import stats
from scipy.special import logsumexp
pio.templates.default = "plotly_white"
pio.renderers.default = os.environ.get("PLOTLY_RENDERER", "notebook")
np.set_printoptions(precision=4, suppress=True)
SEED = 7
rng = np.random.default_rng(SEED)
import sys
import plotly
print("Python:", sys.version.split()[0])
print("NumPy:", np.__version__)
print("SciPy:", scipy.__version__)
print("Plotly:", plotly.__version__)
print("Seed:", SEED)
Python: 3.12.9
NumPy: 1.26.2
SciPy: 1.15.0
Plotly: 6.5.2
Seed: 7
1) Title & Classification#
Name:
randint(random integer / discrete uniform on a contiguous integer interval)Type: Discrete
Support (SciPy convention): $\(x \in \{\ell,\ \ell+1,\ \ldots,\ h-1\}\)\( where \)\ell = \texttt{low}\( and \)h = \texttt{high}$.
Parameter space: $\(\ell \in \mathbb{Z},\quad h \in \mathbb{Z},\quad h > \ell.\)$
It is often convenient to define the number of possible outcomes: $\( n = h - \ell. \)\( Then the support contains exactly \)n$ integers.
Note on SciPy’s loc: many SciPy distributions also accept a loc shift. For randint, loc shifts the support by addition:
$\(
X \sim \texttt{randint}(\ell, h, \texttt{loc})\quad\Rightarrow\quad \text{support } \{\ell+\texttt{loc}, \ldots, h-1+\texttt{loc}\}.
\)$
In this notebook we focus on the common loc=0 case unless stated otherwise.
2) Intuition & Motivation#
This is the discrete analogue of the continuous uniform distribution: all allowed integer values are equally likely.
What it models: an integer outcome with no reason to prefer one value over another within known bounds.
Examples: a fair die roll (with an appropriate mapping), choosing a random index into an array, random day-of-week encoding, random A/B group assignment when the groups are equally sized by design.
Typical real-world use cases:
Random indexing / shuffling: picking a random element from a list.
Simulation: drawing random IDs, discrete time steps, or random augmentation choices.
Bounded non-informative priors in Bayesian modeling for integer-valued parameters (e.g., a model index among \(\{1,\dots,M\}\)).
Relations to other distributions#
Continuous uniform: if \(U \sim \mathrm{Uniform}(0,1)\) and you set \(X = \ell + \lfloor nU \rfloor\), then \(X\) is
randinton \(\{\ell,\dots,h-1\}\).Categorical:
randintis a categorical distribution over consecutive integers with equal probabilities.Bernoulli: when \(n=2\) (two consecutive integers), this is a Bernoulli distribution after a simple re-labeling.
Discrete uniform on \(\{1,\dots,N\}\): setting
low=1,high=N+1gives the familiar “uniform from 1 to N” model.
3) Formal Definition#
Let \(X\) be uniformly distributed on the integers \(\{\ell, \ell+1, \ldots, h-1\}\) where \(\ell,h\in\mathbb{Z}\) and \(h>\ell\). Let \(n = h-\ell\).
PMF#
For integer \(k\), $\( \mathbb{P}(X=k) = \begin{cases} \frac{1}{n} & k \in \{\ell,\ell+1,\ldots,h-1\}\\ 0 & \text{otherwise.} \end{cases} \)$
CDF#
Because this is a discrete distribution, the CDF is a step function. For real \(x\), $\( F(x)=\mathbb{P}(X\le x)= \begin{cases} 0 & x < \ell \\ \frac{\lfloor x\rfloor-\ell+1}{n} & \ell \le x < h-1 \\ 1 & x \ge h-1. \end{cases} \)$
An equivalent “clipped” form (useful for implementation) is: $\( F(x)=\mathrm{clip}\left(\frac{\lfloor x\rfloor-\ell+1}{n},\ 0,\ 1\right). \)$
def _as_int_like(name: str, value) -> int:
if isinstance(value, (int, np.integer)):
return int(value)
if isinstance(value, (float, np.floating)) and float(value).is_integer():
return int(value)
raise TypeError(f"{name} must be an integer (or integer-valued float), got {value!r}")
def validate_low_high(low, high) -> tuple[int, int]:
low = _as_int_like("low", low)
high = _as_int_like("high", high)
if high <= low:
raise ValueError(f"Require high > low, got low={low}, high={high}")
return low, high
def randint_support(low, high) -> np.ndarray:
low, high = validate_low_high(low, high)
return np.arange(low, high)
def randint_pmf(x, low, high):
"""PMF of randint(low, high) evaluated at x."""
low, high = validate_low_high(low, high)
n = high - low
x = np.asarray(x)
is_int = x == np.floor(x)
in_support = is_int & (x >= low) & (x < high)
return np.where(in_support, 1.0 / n, 0.0).astype(float)
def randint_cdf(x, low, high):
"""CDF of randint(low, high) evaluated at x (step function)."""
low, high = validate_low_high(low, high)
n = high - low
x = np.asarray(x, dtype=float)
m = np.floor(x)
cdf = (m - low + 1.0) / n
return np.clip(cdf, 0.0, 1.0)
low_demo, high_demo = 2, 7
x_demo = np.array([1, 2, 3, 6, 7, 8], dtype=float)
print("support:", randint_support(low_demo, high_demo))
print("x:", x_demo)
print("pmf:", randint_pmf(x_demo, low_demo, high_demo))
print("cdf:", randint_cdf(x_demo, low_demo, high_demo))
support: [2 3 4 5 6]
x: [1. 2. 3. 6. 7. 8.]
pmf: [0. 0.2 0.2 0.2 0. 0. ]
cdf: [0. 0.2 0.4 1. 1. 1. ]
4) Moments & Properties#
Let \(n=h-\ell\).
Mean and variance#
Because the distribution is uniform on a finite set, all moments exist.
Mean: $\(\mathbb{E}[X] = \frac{\ell + (h-1)}{2} = \frac{\ell + h - 1}{2}.\)$
Variance: $\(\mathrm{Var}(X) = \frac{n^2-1}{12}.\)$
Skewness and kurtosis#
Skewness: \(0\) (the distribution is symmetric about its mean).
Excess kurtosis (Fisher kurtosis): $\(\gamma_2 = -\frac{6(n^2+1)}{5(n^2-1)}.\)$
MGF and characteristic function#
For \(t\ne 0\), the moment generating function is a finite geometric series: $\( M_X(t)=\mathbb{E}[e^{tX}] = \frac{1}{n}\sum_{k=\ell}^{h-1} e^{tk} = \frac{e^{t\ell}\,(1-e^{tn})}{n(1-e^{t})}. \)\( (And \)M_X(0)=1$ by continuity.)
The characteristic function is the same expression with \(t=i\omega\): $\( \varphi_X(\omega) = \mathbb{E}[e^{i\omega X}] = \frac{e^{i\omega\ell}(1-e^{i\omega n})}{n(1-e^{i\omega})} = e^{i\omega(\ell + (n-1)/2)}\,\frac{\sin(n\omega/2)}{n\sin(\omega/2)}. \)$
Entropy#
Because the distribution is uniform over \(n\) outcomes, $\( H(X) = -\sum_k \mathbb{P}(X=k)\log \mathbb{P}(X=k) = \log n \)\( (in **nats**; use \)\log_2$ for bits).
def randint_moments(low, high):
low, high = validate_low_high(low, high)
n = high - low
mean = 0.5 * (low + high - 1)
var = (n**2 - 1) / 12
skew = 0.0 if n > 1 else np.nan
excess_kurtosis = (-6 * (n**2 + 1) / (5 * (n**2 - 1))) if n > 1 else np.nan
entropy = float(np.log(n))
return mean, var, skew, excess_kurtosis, entropy
def randint_mgf(t, low, high):
"""MGF M_X(t) for randint(low, high) using a stable expm1 ratio."""
low, high = validate_low_high(low, high)
n = high - low
t = np.asarray(t, dtype=float)
out = np.empty_like(t, dtype=float)
mask0 = t == 0
out[mask0] = 1.0
tt = t[~mask0]
out[~mask0] = np.exp(tt * low) * (np.expm1(tt * n) / np.expm1(tt)) / n
return out
low, high = 2, 7
mean, var, skew, ex_kurt, ent = randint_moments(low, high)
rv = stats.randint(low, high)
print("Formulas:")
print(" mean:", mean)
print(" var :", var)
print(" skew:", skew)
print(" excess kurtosis:", ex_kurt)
print(" entropy (nats):", ent)
print()
print("SciPy:")
print(" stats(mvsk):", rv.stats(moments="mvsk"))
print(" entropy :", rv.entropy())
# Numerical check of the MGF against Monte Carlo
n_mc = 200_000
t = 0.3
x_mc = rv.rvs(size=n_mc, random_state=rng)
mgf_emp = np.mean(np.exp(t * x_mc))
mgf_theo = float(randint_mgf(t, low, high))
print()
print("MGF check at t=0.3:")
print(" empirical E[exp(tX)]:", mgf_emp)
print(" theoretical M_X(t) :", mgf_theo)
Formulas:
mean: 4.0
var : 2.0
skew: 0.0
excess kurtosis: -1.3
entropy (nats): 1.6094379124341003
SciPy:
stats(mvsk): (4.0, 2.0, 0.0, -1.3)
entropy : 1.6094379124341003
MGF check at t=0.3:
empirical E[exp(tX)]: 3.6290750483186573
theoretical M_X(t) : 3.626635073807003
5) Parameter Interpretation#
randint has two boundary parameters:
\(\ell=\texttt{low}\) sets the left endpoint (included).
\(h=\texttt{high}\) sets the right endpoint (excluded).
\(n=h-\ell\) is the support size (how many integers are possible).
How parameters change the distribution:
Changing \(\ell\) with fixed \(n\) shifts the distribution left/right but does not change its shape.
Increasing \(n\) (moving \(h\) farther from \(\ell\)) spreads mass over more integers, decreasing each point probability from \(1/n\).
A useful mental model is: $\( X = \ell + Y,\qquad Y \sim \text{Uniform on } \{0,1,\ldots,n-1\}. \)\( Shifts affect the mean; the variance depends only on \)n$.
6) Derivations#
Let \(X\) be uniform on \(\{\ell,\ell+1,\ldots,h-1\}\) and let \(n=h-\ell\).
Expectation#
Write \(X=\ell+Y\) where \(Y\) is uniform on \(\{0,1,\ldots,n-1\}\). Then $\( \mathbb{E}[X]=\ell+\mathbb{E}[Y]. \)\( Compute \)\( \mathbb{E}[Y]=\frac{1}{n}\sum_{k=0}^{n-1} k = \frac{1}{n}\cdot\frac{(n-1)n}{2}=\frac{n-1}{2}, \)\( so \)\( \mathbb{E}[X]=\ell+\frac{n-1}{2}=\frac{\ell+h-1}{2}. \)$
Variance#
Because variance is shift-invariant, \(\mathrm{Var}(X)=\mathrm{Var}(Y)\). Use \(\mathrm{Var}(Y)=\mathbb{E}[Y^2]-(\mathbb{E}[Y])^2\).
First, $\( \mathbb{E}[Y^2]=\frac{1}{n}\sum_{k=0}^{n-1} k^2 =\frac{1}{n}\cdot\frac{(n-1)n(2n-1)}{6} = \frac{(n-1)(2n-1)}{6}. \)\( Then \)\( \mathrm{Var}(Y)=\frac{(n-1)(2n-1)}{6}-\left(\frac{n-1}{2}\right)^2 =\frac{n^2-1}{12}. \)$
Likelihood (i.i.d. sample)#
Let \(x_1,\ldots,x_m\) be i.i.d. draws from randint(\ell,h).
The likelihood is
$\(
L(\ell,h; x_{1:m})=\prod_{i=1}^m \mathbb{P}(X=x_i)
=\begin{cases}
\left(\frac{1}{h-\ell}\right)^m & \text{if all } x_i \in \{\ell,\ldots,h-1\} \\
0 & \text{otherwise.}
\end{cases}
\)$
So the log-likelihood (when feasible) is $\( \ell(\ell,h) = -m\log(h-\ell). \)\( To maximize it, we want the **smallest interval** that still contains the data. Let \)x_{\min}=\min_i x_i\( and \)x_{\max}=\max_i x_i\(. The unique MLE under the half-open support convention is: \)\( \hat{\ell}=x_{\min},\qquad \hat{h}=x_{\max}+1. \)$
def randint_log_likelihood(low, high, x) -> float:
"""Log-likelihood for i.i.d. data x under randint(low, high)."""
low, high = validate_low_high(low, high)
x = np.asarray(x)
if x.size == 0:
raise ValueError("x must be non-empty")
if not np.all(x == np.floor(x)):
raise ValueError("x must contain integer-valued observations")
x_min = x.min()
x_max = x.max()
if (x_min < low) or (x_max >= high):
return -np.inf
n = high - low
return -x.size * float(np.log(n))
def randint_mle(x) -> tuple[int, int]:
x = np.asarray(x)
if x.size == 0:
raise ValueError("x must be non-empty")
if not np.all(x == np.floor(x)):
raise ValueError("x must contain integer-valued observations")
low_hat = int(x.min())
high_hat = int(x.max()) + 1
return low_hat, high_hat
# Simulate data and visualize how the likelihood prefers the tightest interval.
low_true, high_true = 3, 11
m = 60
x = stats.randint.rvs(low_true, high_true, size=m, random_state=rng)
low_hat, high_hat = randint_mle(x)
print("true (low, high):", (low_true, high_true))
print("MLE (low, high):", (low_hat, high_hat))
low_grid = np.arange(low_hat - 6, low_hat + 1)
high_grid = np.arange(high_hat, high_hat + 7)
ll = np.empty((low_grid.size, high_grid.size), dtype=float)
for i, lo in enumerate(low_grid):
for j, hi in enumerate(high_grid):
ll[i, j] = randint_log_likelihood(lo, hi, x)
ll_plot = ll.copy()
ll_plot[~np.isfinite(ll_plot)] = np.nan
fig = go.Figure(
data=go.Heatmap(
z=ll_plot,
x=high_grid,
y=low_grid,
colorbar_title="log L",
)
)
fig.add_trace(
go.Scatter(
x=[high_hat],
y=[low_hat],
mode="markers",
marker=dict(color="red", size=10),
name="MLE",
)
)
fig.update_layout(
title="Log-likelihood over candidate (low, high) intervals",
xaxis_title="high (exclusive)",
yaxis_title="low",
)
fig.show()
true (low, high): (3, 11)
MLE (low, high): (3, 11)
7) Sampling & Simulation (NumPy-only)#
A simple sampler uses a uniform random variable and a floor operation.
Draw \(U \sim \mathrm{Uniform}(0,1)\).
Return $\(X = \ell + \lfloor nU \rfloor,\quad n=h-\ell.\)$
Why this works: \(\lfloor nU \rfloor\) takes values in \(\{0,\ldots,n-1\}\), and each integer interval \([k/n,(k+1)/n)\) has probability \(1/n\).
This is also the logic behind rng.integers(low, high).
def sample_randint_numpy(low, high, size, rng: np.random.Generator | None = None):
"""Sample from randint(low, high) using only NumPy primitives."""
low, high = validate_low_high(low, high)
n = high - low
if rng is None:
rng = np.random.default_rng()
u = rng.random(size=size) # Uniform on [0,1)
return (low + np.floor(n * u)).astype(int)
low, high = 2, 7
n = 20_000
x = sample_randint_numpy(low, high, size=n, rng=rng)
values = np.arange(low, high)
emp_pmf = np.array([(x == v).mean() for v in values])
theo_pmf = np.full_like(emp_pmf, 1.0 / (high - low), dtype=float)
print("values:", values)
print("empirical pmf:", emp_pmf)
print("theoretical pmf:", theo_pmf)
values: [2 3 4 5 6]
empirical pmf: [0.1956 0.2067 0.1962 0.2079 0.1935]
theoretical pmf: [0.2 0.2 0.2 0.2 0.2]
8) Visualization#
We’ll visualize:
the PMF for several
(low, high)choicesthe CDF (step function)
Monte Carlo samples: the empirical PMF compared to the theoretical PMF
# PMF for several parameter choices
params_list = [(0, 6), (2, 8), (0, 11)] # (low, high)
x_grid = np.arange(-1, 12)
fig_pmf = go.Figure()
for low, high in params_list:
fig_pmf.add_trace(
go.Bar(
name=f"low={low}, high={high}",
x=x_grid,
y=randint_pmf(x_grid, low, high),
)
)
fig_pmf.update_layout(
title="randint PMF for different (low, high)",
xaxis_title="x",
yaxis_title="P(X = x)",
barmode="group",
)
fig_pmf.show()
# CDF for the same parameters
x_cont = np.linspace(-1.0, 12.0, 700)
fig_cdf = go.Figure()
for low, high in params_list:
fig_cdf.add_trace(
go.Scatter(
name=f"low={low}, high={high}",
x=x_cont,
y=randint_cdf(x_cont, low, high),
mode="lines",
line_shape="hv",
)
)
fig_cdf.update_layout(
title="randint CDF (step function)",
xaxis_title="x",
yaxis_title="F(x)",
)
fig_cdf.show()
# Monte Carlo: empirical PMF vs theoretical PMF
low, high = 2, 7
n = 50_000
x = sample_randint_numpy(low, high, size=n, rng=rng)
values = np.arange(low, high)
emp = np.array([(x == v).mean() for v in values])
theo = np.full_like(emp, 1.0 / (high - low), dtype=float)
fig_mc = go.Figure()
fig_mc.add_trace(go.Bar(name="empirical", x=values, y=emp))
fig_mc.add_trace(go.Bar(name="theoretical", x=values, y=theo))
fig_mc.update_layout(
title=f"Monte Carlo check (n={n:,}) for randint(low={low}, high={high})",
xaxis_title="x",
yaxis_title="probability",
barmode="group",
)
fig_mc.show()
9) SciPy Integration#
SciPy provides this distribution as scipy.stats.randint.
PMF / CDF:
stats.randint.pmf,stats.randint.cdfSampling:
stats.randint.rvsor frozenrv = stats.randint(low, high)thenrv.rvs(...)Entropy and moments:
rv.entropy(),rv.stats(moments="mvsk")
Fitting#
randint has integer shape parameters with unbounded domains, so scipy.stats.fit requires finite bounds.
For this distribution, you often have an analytic MLE:
$\(
\hat{\ell}=\min_i x_i,\qquad \hat{h}=\max_i x_i+1.
\)$
randint = stats.randint
low, high = 2, 7
print("pmf(low..high):", randint.pmf(np.arange(low, high), low, high))
print("cdf(low-1..high):", randint.cdf([low - 1, low, high - 1, high], low, high))
rv = randint(low, high)
samples = rv.rvs(size=10, random_state=rng)
print("rvs:", samples)
# Analytic MLE from a sample
data = randint.rvs(5, 13, size=2_000, random_state=rng)
low_hat, high_hat = randint_mle(data)
print()
print("Analytic MLE:")
print(" low_hat =", low_hat)
print(" high_hat=", high_hat)
# scipy.stats.fit requires finite bounds (and returns float-valued parameters)
fit_res = stats.fit(
randint,
data,
bounds={
"low": (low_hat - 5, low_hat),
"high": (high_hat, high_hat + 5),
"loc": (0, 0),
},
)
print()
print("scipy.stats.fit result:")
print(fit_res)
print(" (low, high, loc) =", (fit_res.params.low, fit_res.params.high, fit_res.params.loc))
pmf(low..high): [0.2 0.2 0.2 0.2 0.2]
cdf(low-1..high): [0. 0.2 1. 1. ]
rvs: [3 3 6 2 3 3 4 5 5 6]
Analytic MLE:
low_hat = 5
high_hat= 13
scipy.stats.fit result:
params: FitParams(low=5.0, high=13.0, loc=0.0)
success: True
message: 'Optimization terminated successfully.'
(low, high, loc) = (5.0, 13.0, 0.0)
10) Statistical Use Cases#
Hypothesis testing (goodness-of-fit to uniformity)#
If you believe outcomes should be equally likely across a fixed integer set, you can test whether observed counts match a uniform distribution using a chi-square goodness-of-fit test.
Bayesian modeling (bounded discrete prior)#
A discrete uniform distribution is a natural prior over a bounded set of integer hypotheses. A classic example is the German tank problem: serial numbers are modeled as i.i.d. uniform draws from \(\{1,\ldots,N\}\) with unknown \(N\).
Generative modeling (uniform mixture weights / random indices)#
In generative models, you often sample an index uniformly:
choose a mixture component uniformly
choose a data augmentation option uniformly
choose a random class label for synthetic data
# --- Hypothesis testing: chi-square goodness-of-fit to a known uniform support ---
low, high = 0, 10
n = 2_000
x = sample_randint_numpy(low, high, size=n, rng=rng)
values = np.arange(low, high)
counts = np.array([(x == v).sum() for v in values])
expected = np.full_like(counts, n / (high - low), dtype=float)
chi2, p_value = stats.chisquare(f_obs=counts, f_exp=expected)
print("Chi-square test for uniformity")
print(" chi2 statistic:", chi2)
print(" p-value :", p_value)
# --- Bayesian modeling: German tank problem (posterior over N) ---
# Model: serials ~ Uniform{1,2,...,N} i.i.d. (inclusive upper bound)
# Map to SciPy's randint with low=1, high=N+1.
N_true = 200
n_obs = 15
serials = stats.randint.rvs(1, N_true + 1, size=n_obs, random_state=rng)
max_serial = int(serials.max())
N_max = 600 # prior upper limit
N_grid = np.arange(max_serial, N_max + 1)
# Uniform prior over N_grid: p(N) = constant.
# Likelihood: p(data | N) = 1/N^n_obs for N >= max_serial, else 0.
log_post_unnorm = -n_obs * np.log(N_grid)
log_post = log_post_unnorm - logsumexp(log_post_unnorm)
post = np.exp(log_post)
post_mean = float(np.sum(N_grid * post))
post_map = int(N_grid[np.argmax(post)])
cdf = np.cumsum(post)
ci_low = int(N_grid[np.searchsorted(cdf, 0.025)])
ci_high = int(N_grid[np.searchsorted(cdf, 0.975)])
print()
print("German tank example")
print(" true N :", N_true)
print(" max observed :", max_serial)
print(" posterior mean:", round(post_mean, 2))
print(" posterior MAP :", post_map)
print(" 95% credible interval:", (ci_low, ci_high))
fig = go.Figure()
fig.add_trace(go.Scatter(x=N_grid, y=post, mode="lines", name="posterior"))
fig.add_vline(x=N_true, line_dash="dash", line_color="gray", annotation_text="N_true")
fig.add_vline(x=max_serial, line_dash="dash", line_color="red", annotation_text="max serial")
fig.update_layout(
title="Posterior over N in the German tank problem (uniform prior)",
xaxis_title="N",
yaxis_title="posterior probability",
)
fig.show()
# --- Generative modeling: uniform mixture component index ---
K = 3
means = np.array([-2.0, 0.0, 3.0])
sigma = 0.6
n = 5_000
z = sample_randint_numpy(0, K, size=n, rng=rng) # component index in {0,1,...,K-1}
y = rng.normal(loc=means[z], scale=sigma)
fig = go.Figure()
fig.add_trace(go.Histogram(x=y, nbinsx=60, name="samples"))
fig.update_layout(
title="Samples from a 3-component Gaussian mixture with uniform weights",
xaxis_title="y",
yaxis_title="count",
)
fig.show()
Chi-square test for uniformity
chi2 statistic: 10.2
p-value : 0.3345381516340064
German tank example
true N : 200
max observed : 196
posterior mean: 210.54
posterior MAP : 196
95% credible interval: (196, 254)
11) Pitfalls#
Inclusive vs exclusive upper bound:
SciPy/NumPy use a half-open interval:
lowis included andhighis excluded.Python’s
random.randint(a, b)is inclusive on both ends.
Invalid parameters: you must have
high > low. Ifhigh == low + 1, the distribution is degenerate (always returnslow).Fitting requires bounds:
scipy.stats.fit(stats.randint, data)fails without finite bounds becauselowandhighare unbounded a priori.Non-integer parameters/observations: SciPy returns
naniflow/highare not integer-valued.MGF overflow: \(M_X(t)\) can overflow for large positive \(t\); use it for modest \(t\) and prefer characteristic functions for numerical stability.
# Demonstrate the 'fit requires bounds' pitfall
x = stats.randint.rvs(2, 7, size=500, random_state=rng)
try:
stats.fit(stats.randint, x)
except Exception as e:
print("stats.fit(stats.randint, x) failed as expected:")
print(" ", type(e).__name__ + ":", e)
# Provide finite bounds to make it work
low_hat, high_hat = randint_mle(x)
fit_res = stats.fit(
stats.randint,
x,
bounds={"low": (low_hat - 3, low_hat), "high": (high_hat, high_hat + 3), "loc": (0, 0)},
)
print()
print("fit with bounds:")
print(fit_res)
stats.fit(stats.randint, x) failed as expected:
ValueError: The intersection of user-provided bounds for `low` and the domain of the distribution is not finite. Please provide finite bounds for shape `low` in `bounds`.
fit with bounds:
params: FitParams(low=2.0, high=7.0, loc=0.0)
success: True
message: 'Optimization terminated successfully.'
12) Summary#
randint(low, high)is a discrete uniform distribution on \(\{\texttt{low},\ldots,\texttt{high}-1\}\).PMF: \(\mathbb{P}(X=k)=1/(h-\ell)\) for integers \(k\) in the support.
Mean: \((\ell+h-1)/2\); Variance: \(((h-\ell)^2-1)/12\); Entropy: \(\log(h-\ell)\).
A simple NumPy sampler is \(X=\ell+\lfloor (h-\ell)U\rfloor\) with \(U\sim\mathrm{Uniform}(0,1)\).
The likelihood for i.i.d. data is proportional to \((h-\ell)^{-m}\) when the interval contains all observations, so the MLE is \((\min x_i,\ \max x_i+1)\).
In SciPy,
scipy.stats.fitneeds finite bounds forrandintbecause parameters are unbounded.